package nels4561;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import spacewar2.actions.MoveAction;
import spacewar2.actions.MoveToObjectAction;
import spacewar2.actions.SpacewarAction;
import spacewar2.actions.SpacewarActionException;
import spacewar2.actions.SpacewarPurchaseEnum;
import spacewar2.clients.TeamClient;
import spacewar2.objects.Asteroid;
import spacewar2.objects.Base;
import spacewar2.objects.Beacon;
import spacewar2.objects.Ship;
import spacewar2.objects.SpacewarActionableObject;
import spacewar2.objects.SpacewarObject;
import spacewar2.powerups.SpacewarPowerup;
import spacewar2.powerups.SpacewarPowerupEnum;
import spacewar2.shadows.Shadow;
import spacewar2.simulator.Toroidal2DPhysics;
import spacewar2.utilities.Position;

/**
 * Assigns each member of the team to collect an asteroid using A*
 * Beacons and bases are navigated to when necessary 
 * 
 * @author Michael & Thomas
 */
public class AStarAgent extends TeamClient 
{
	/**
	 * Map of the action to which the ship is performing
	 */
	HashMap<Ship, ArrayList<GridCell>> shipToPathMap;
	/**
	 * The number of timeSteps
	 */
	public int timeSteps;
	/**
	 * The target the ship is aiming for
	 */
	SpacewarObject target;
	
	/**
	 * Shooting fields
	 */
	Random random;
	public static double SHOOT_PROBABILITY = 0.1;
	
	/**
	 * Used temporarily to decide whether or not to shoot enemy ships.
	 * Could still be useful in future.
	 * */
	static final double SHIP_ATTACK_DISTANCE = 250.0;
	static final double MAX_SHIP_ANGLE = 5.0;
	
	/**
	 * ArrayList of ship targets for the ship to choose from.
	 * Each target is in the form of an Egocentric Representation.
	 * Each Egocentric Representation contains a target object and a target value.
	 * */
	ArrayList<EgocentricRepresentation> shipTargets;
	
	/**
	 * Send each ship to a target
	 */
	public Map<UUID, SpacewarAction> getMovementStart(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) 
			{
		//increment timeSteps
		timeSteps++;
		HashMap<UUID, SpacewarAction> actions = new HashMap<UUID, SpacewarAction>();
		
		
		MoveAction action = null;
		for (SpacewarObject actionable :  actionableObjects)
		{
			if (actionable instanceof Ship) 
			{
				//create cast actionable as our ship
				Ship ship = (Ship) actionable;
				
				//find ship's current path
				ArrayList<GridCell> currentPath = shipToPathMap.get(ship);
			
				//create new grid for timeStep
				Grid myGrid = new Grid(space, ship);

				//do we need to recalculate our a*?
				if (currentPath == null || timeSteps%10==0)
				{			
					//pick our next target
					target = decideNextTarget(space, ship);
					
					//populate our grid using the target
					myGrid.populateGrid(space, target);
					
					//calculate path to target using a*
					GridCell currentCell = myGrid.getCurrentCell();
					ArrayList<GridCell> pathList = CalculateActions(currentCell, target, myGrid, space);
					Position nextLoc = pathList.get(0)._location;
					
					//if we are at a target, use MoveToObjectAction to make cleaner transactions between objects
					//else use regular MoveAction
					if(currentCell.object != null && currentCell.object.getId() == target.getId())
						action = new MoveToObjectAction(space, ship.getPosition(), target);
					else {
						action = new MoveAction(space, currentCell._location, nextLoc, 
								space.findShortestDistanceVector(currentCell._location, nextLoc).multiply(2.0));
					}
					
					//map ship to new currentPath
					shipToPathMap.put(ship, pathList);
				}
				else
				{
					GridCell nextPathCell = currentPath.get(0);
					
					//remove gridCell from path if we are in it
					if(myGrid.getCurrentCell() == nextPathCell)
					{
						currentPath.remove(0);
					}
					
					//if we are at a target, use MoveToObjectAction to make cleaner transactions between objects
					//else use regular MoveAction
					if(nextPathCell.object != null && nextPathCell.object.getId() == target.getId())
						action = new MoveToObjectAction(space, ship.getPosition(), target);
					else
					{
						action = new MoveAction(space, ship.getPosition(), nextPathCell._location,
								space.findShortestDistanceVector(ship.getPosition(), nextPathCell._location).multiply(2.0));
					}
					
					//map ship to new currentPath
					shipToPathMap.put(ship, currentPath);
				}
				//put new action in actions map
				actions.put(ship.getId(), action);
			}
		}
		
		return actions;
	}
	/**
	 * Populate all potential targets into the shipTargets ArrayList.
	 * For each potential target in the environment, make an Egocentric Representation out of it.
	 * 
	 * @param space: spacewar environment
	 * @param ship: current ship
	 * */
	private void populateShipTargets(Toroidal2DPhysics space, Ship ship)
	{
		EgocentricRepresentation target;
		
		//calculate all potential Beacon targets
		for(Beacon beacon : space.getBeacons())
		{
			target = new EgocentricRepresentation(beacon, space, ship, timeSteps);
			shipTargets.add(target);
		}
		//calculate all potential Base targets
		for(Base base : space.getBases())
		{
			if(base.getTeamName() == this.getTeamName())
			{
				target = new EgocentricRepresentation(base, space, ship, timeSteps);
				shipTargets.add(target);
			}
		}
		//calculate all potential Asteroid targets
		for(Asteroid asteroid : space.getAsteroids())
		{
			if(asteroid.isMineable())
			{
				target = new EgocentricRepresentation(asteroid, space, ship, timeSteps);
				shipTargets.add(target);
			}
		}
		//calculate all potential Ship targets
		for(Ship loopShip : space.getShips())
		{
			if(loopShip.getTeamName() != ship.getTeamName())
			{
				target = new EgocentricRepresentation(loopShip, space, ship, timeSteps);
				shipTargets.add(target);
			}
		}
	}
	
	/**
	 * Sorts all potential ship targets based on their target values.
	 * */
	private void sortShipTargets()
	{
		//compare Ego Reps in shipTargets using Ego Rep target balues
		Collections.sort(shipTargets, new Comparator<EgocentricRepresentation>(){

			@Override
			public int compare(EgocentricRepresentation arg0,
					EgocentricRepresentation arg1) {
				if(arg0.targetValue < arg1.targetValue)
					return 1;
				return -1;
			}
			
		});
	}
	
	/**
	 * Chooses next target for agent based on target values.
	 * Target values provided by Global and Egocentric Representations
	 * 
	 * @param space: spacewar environment
	 * @param ship: current ship
	 * */
	public SpacewarObject decideNextTarget(Toroidal2DPhysics space, Ship ship)
	{
		//local variables
		SpacewarObject target;
		SpacewarObject clusterTarget;
		Cluster bestCluster;
		
		//populate the Egocentric Representations
		populateShipTargets(space, ship);
		sortShipTargets();
		
		//create a new Global Representation
		GlobalRepresentation globalRep = new GlobalRepresentation(space, ship);
		bestCluster = globalRep.allClusters.get(0);
		
		//if the cluster contains a beacon, head there first just to be safe
		if(bestCluster.clusterBeacon == null)
		{
			clusterTarget = bestCluster.clusterHub;
		}
		else
		{
			clusterTarget = bestCluster.clusterBeacon;
		}
		
		
		//choose the highest value between the Global and Ego Representations
		if(shipTargets.get(0).targetValue > bestCluster.clusterValue)
			target = shipTargets.get(0).target;
		else
		{
			target = clusterTarget;
		}
		
		//slear ship targets for next timeStep
		shipTargets.clear();
		
		return target;
	}

	/**
	 * Use A* to calculate a path to the current target and search adjacent cells for local targets.
	 * 
	 * @param current: current gridCell of the ship
	 * @param goal: agent's target
	 * @param myGrid: grid for ship
	 * @param space: spacewar environment
	 * @return ArrayList<SpacewarAction>
	 */
	public ArrayList<GridCell> CalculateActions(GridCell current, SpacewarObject goal, Grid myGrid, Toroidal2DPhysics space)
	{
		//function variables
		ArrayList<GridCell> path = new ArrayList<GridCell>();
		//gridcell that aStar is currently in
		GridCell aStarLocation = current;
		int g = 0;
		
		while (true)
		{
			//if the path is too long, break out
			if (g > 150)
			{
				return path;
			}
			
			//find neighbors for aStarLocation
			ArrayList<GridCell> neighborsList = aStarLocation.getNeighborsWithinRadius(myGrid, 1);
			GridCell bestAdjacentCell = null;
			
			//sort the neighbors based on their h values
			Collections.sort(neighborsList, new Comparator<GridCell>() {
			    public int compare(GridCell a, GridCell b) {
			    	if (a.h > b.h)
			    	{
			    		return 1;
			    	}
			    	return -1;
			    }
			});
			
			//if the path finds a different asteroid first, set path for it instead
			for(GridCell neighbor : neighborsList)
			{
				if(neighbor.containsMineableAsteroid)
				{
					bestAdjacentCell = neighbor;
					path.add(bestAdjacentCell);
					return path;
				}
				//if aStar finds target, end algorithm
				if(neighbor.object != null)
				{
					if (neighbor.object.getId() == goal.getId())
					{
						bestAdjacentCell = neighbor;
						path.add(bestAdjacentCell);
						return path;
					}
				}
			}
			
			//if it doesn't find any targets, pick next best cell to go in
			GridCell bestFreeCell = null;
			for (int i = 0; i < neighborsList.size(); i++)
			{
				bestFreeCell = neighborsList.get(i);
				if (!bestFreeCell.avoid)
				{
					break;
				}
			}
			
			//update aStarLocation
			aStarLocation = bestFreeCell;
			//add cell to path to be returned
			path.add(bestFreeCell);
			//increment path cost g
			g++;
		}
	}
	
	/**
	 * Returns the current timeStep that the simulation is on.
	 * 
	 * @return timeSteps
	 * */
	int getTimeSteps()
	{
		return timeSteps;
	}

	@Override
	public void initialize() 
	{
		//Shooting
		random = new Random();
		
		//Initialize Class Variables
		timeSteps = 0;
		shipToPathMap = new HashMap<Ship, ArrayList<GridCell>>();		
		shipTargets = new ArrayList<EgocentricRepresentation>();
		target = null;
	}

	@Override
	public void shutDown() {

	}

	@Override
	public void getMovementEnd(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) 
	{

	}

	@Override
	public Map<UUID, SpacewarPowerupEnum> getPowerups(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) {
		
		Map<UUID, SpacewarPowerupEnum> powerupMap = new HashMap<UUID, SpacewarPowerupEnum>();
		
		/*for (SpacewarObject actionable :  actionableObjects) {
			if (actionable instanceof Ship) {
				Ship myShip = (Ship) actionable;
				if(target instanceof Ship)
				{
					for(Ship ship : space.getShips())
					{
						if(ship.getTeamName() != (myShip.getTeamName()))
						{
							if (random.nextDouble() < SHOOT_PROBABILITY && 
									(space.findShortestDistance(ship.getPosition(), target.getPosition()) < 
											space.findShortestDistance(myShip.getPosition(), target.getPosition()))) {
								powerupMap.put(myShip.getId(), myShip.getNewBullet());
								//System.out.println("Firing!");
							}
						}
					}
				}
			}
		}*/
		return powerupMap;
	}

	/**
	 * Purchases new base when there is enough money.
	 * 
	 * @param space: spacewar environment
	 * @param ships: team ships
	 * @param availableMoney: amount of money the player has
	 * @param currentCostNewBase: obvious
	 * */
	@Override
	public Map<UUID, SpacewarPurchaseEnum> getTeamPurchases(
			Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects,
			int availableMoney, Map<SpacewarPurchaseEnum, Integer> purchaseCosts) {
		
		HashMap<UUID, SpacewarPurchaseEnum> purchases = new HashMap<UUID, SpacewarPurchaseEnum>();
		double BASE_BUYING_DISTANCE = 200;
		boolean bought_base = false;

		if (availableMoney >= purchaseCosts.get(SpacewarPurchaseEnum.BASE)) {
			for (SpacewarActionableObject actionableObject : actionableObjects) {
				if (actionableObject instanceof Ship) {
					Ship ship = (Ship) actionableObject;
					Set<Base> bases = space.getBases();

					// how far away is this ship to a base of my team?
					double maxDistance = Double.MIN_VALUE;
					for (Base base : bases) {
						if (base.getTeamName().equalsIgnoreCase(getTeamName())) {
							double distance = space.findShortestDistance(ship.getPosition(), base.getPosition());
							if (distance > maxDistance) {
								maxDistance = distance;
							}
						}
					}

					if (maxDistance > BASE_BUYING_DISTANCE) {
						purchases.put(ship.getId(), SpacewarPurchaseEnum.BASE);
						bought_base = true;
						//System.out.println("Buying a base!!");
						break;
					}
				}
			}		
		} 
		
		// see if you can buy EMPs
		if (availableMoney >= purchaseCosts.get(SpacewarPurchaseEnum.POWERUP_EMP_LAUNCHER)) {
			for (SpacewarActionableObject actionableObject : actionableObjects) {
				if (actionableObject instanceof Ship) {
					Ship ship = (Ship) actionableObject;
					
					if (ship.isValidPowerup(SpacewarPurchaseEnum.POWERUP_EMP_LAUNCHER.getPowerupMap())) {
						purchases.put(ship.getId(), SpacewarPurchaseEnum.POWERUP_EMP_LAUNCHER);
					}
				}
			}		
		} 
		

		// can I buy a ship?
		if (availableMoney >= purchaseCosts.get(SpacewarPurchaseEnum.SHIP) && bought_base == false) {
			for (SpacewarActionableObject actionableObject : actionableObjects) {
				if (actionableObject instanceof Base) {
					Base base = (Base) actionableObject;
					
					purchases.put(base.getId(), SpacewarPurchaseEnum.SHIP);
					break;
				}

			}

		}


		return purchases;
	}
	@Override
	public Set<Shadow> getShadows() {
		// TODO Auto-generated method stub
		return null;
	}

}
